﻿using System.Collections.Generic;

namespace Gamelight.Graphics.SpriteSheet
{
	public enum Direction
	{
		Left,
		Right,
		Up,
		Down
	}

	public enum HorizontalAlignment
	{
		Left,
		Right,
		Center,
		Autodetect
	}

	public enum VerticalAlignment
	{
		Top,
		Bottom,
		Center,
		Autodetect
	}

	public class SpriteSheet
	{
		private Image sheet;
		private int sheetWidth;
		private int sheetHeight;

		private Dictionary<string, SpriteGroup> spriteGroups = new Dictionary<string, SpriteGroup>();

		/// <summary>
		/// Creates a sprite sheet object. If the image passed in does not have a 
		/// transparent background, the background color will be auto-detected and
		/// converted to transparency. 
		/// </summary>
		/// <param name="spriteSheetImage">Image containing sprites</param>
		/// <param name="alreadyContainsTransparency">true if the image file has a transparent background. </param>
		public SpriteSheet(Image spriteSheetImage, bool alreadyContainsTransparency)
		{
			Color bgColor;
			if (alreadyContainsTransparency)
			{
				bgColor = Colors.Transparent;
			}
			else
			{
				bgColor = this.AutodetectTransparentBackgroundColor(spriteSheetImage);
			}

			this.sheet = this.ClearOutTransparency(spriteSheetImage, bgColor);
			this.sheetWidth = this.sheet.Width;
			this.sheetHeight = this.sheet.Height;
		}

		/// <summary>
		/// Creates a sprite sheet object. 
		/// </summary>
		/// <param name="spriteSheetImage">Image containing sprites</param>
		/// <param name="transparent">The background color of the image (will be converted to transparency).</param>
		public SpriteSheet(Image spriteSheetImage, Color transparent)
		{
			this.sheet = this.ClearOutTransparency(spriteSheetImage, transparent);
			this.sheetWidth = this.sheet.Width;
			this.sheetHeight = this.sheet.Height;
		}

		private Direction direction = Direction.Right;

		public void SetInputDirection(Direction direction)
		{
			this.direction = direction;
		}

		private string GetGroupKey(string name, Direction direction)
		{
			return name + direction.ToString();
		}

		private SpriteGroup GetGroup(string name, Direction direction, bool createIfNonexistent)
		{
			SpriteGroup group;
			string key = this.GetGroupKey(name, direction);
			if (!this.spriteGroups.TryGetValue(key, out group))
			{
				if (createIfNonexistent)
				{
					group = new SpriteGroup(name);
					this.spriteGroups.Add(key, group);
				}
				else
				{
					return null;
				}
			}

			return group;
		}

		/// <summary>
		/// This will add a sprite to the given sprite group. The X and Y coordinates can be any arbitrary coordinates on the
		/// sprite image. The region that will be used will be the smallest rectangular area containing that pixel that has
		/// at least a 1 pixel wide border of transparent pixels around it. 
		/// The direction of the sprite is the last direction used in SetInputDirection
		/// <seealso cref="SetInputDirection"/>
		/// </summary>
		/// <param name="groupName">Name of the sprite group to add this image to. e.g. "jump", "shoot". Do not include the direction. Use SetInputDirection for that.</param>
		/// <param name="someX">The X coordinate of an arbitrary pixel on the sprite</param>
		/// <param name="someY">The Y coordinate of an arbitrary pixel on the sprite</param>
		/// <param name="xOffset">After all alignments are applied, the amount of horizontal offset to shift the picture to properly align it. Use this to tweak its placement.</param>
		/// <param name="yOffset">After all alignments are applied, the amount of vertical offset to shift the picture to properly align it. Use this to tweak its placement.</param>
		public void AddImage(string groupName, 
			int someX, int someY, int xOffset, int yOffset)
		{
			SpriteGroup group = this.GetGroup(groupName, this.direction, true);
			Image image = this.ExtractSprite(someX, someY);

			group.AddSprite(image, direction, xOffset, yOffset);
		}

		/// <summary>
		/// This will add a sprite to the given sprite group. The X and Y coordinates can be any arbitrary coordinates on the
		/// sprite image. The region that will be used will be the smallest rectangular area containing that pixel that has
		/// at least a 1 pixel wide border of transparent pixels around it. 
		/// The direction of the sprite is the last direction used in SetInputDirection
		/// <seealso cref="SetInputDirection"/>
		/// </summary>
		/// <param name="groupName">Name of the sprite group to add this image to. e.g. "jump", "shoot". Do not include the direction. Use SetInputDirection for that.</param>
		/// <param name="someX">The X coordinate of an arbitrary pixel on the sprite</param>
		/// <param name="someY">The Y coordinate of an arbitrary pixel on the sprite</param>
		public void AddImage(string groupName, int someX, int someY)
		{
			this.AddImage(groupName, someX, someY, 0, 0);
		}

		public Image[] GetImages(string groupName, Direction direction)
		{
			SpriteGroup group = this.GetGroup(groupName, direction, false);
			if (group == null)
			{
				Direction flipped;
				if (direction == Direction.Right) flipped = Direction.Left;
				else if (direction == Direction.Left) flipped = Direction.Right;
				else if (direction == Direction.Up) flipped = Direction.Up;
				else flipped = Direction.Down;
				group = this.GetGroup(groupName, flipped, false);
				if (group == null)
				{
					throw new InvalidOperationException("That direction or its mirror does not exist in the sprite sheet");
				}

				group = group.Flip(flipped == Direction.Left || flipped == Direction.Right); ;
				this.spriteGroups[this.GetGroupKey(groupName, direction)] = group;
			}

			return group.Images;
		}

		public void SetAlignment(string groupName, HorizontalAlignment horizontalAlignment, VerticalAlignment verticalAlignment)
		{
			List<SpriteGroup> groups = new List<SpriteGroup>();

			foreach (Direction d in new Direction[] { Direction.Down, Direction.Left, Direction.Right, Direction.Up })
			{
				SpriteGroup group = this.GetGroup(groupName, d, false);
				if (group != null)
				{
					this.SetAlignment(groupName, d, horizontalAlignment, verticalAlignment);
					groups.Add(group);
				}
			}

			if (horizontalAlignment == HorizontalAlignment.Autodetect)
			{
				SpriteGroup.AutodetectCollectiveHorizontalOffsets(groups.ToArray());
			}
		}

		public void AutoDetectCollectiveHorizontalOffsets(ICollection<string> groups, ICollection<Direction> directions)
		{
			List<SpriteGroup> spriteGroups = new List<SpriteGroup>();

			foreach (string groupName in groups)
			{
				foreach (Direction direction in directions)
				{
					SpriteGroup group = this.GetGroup(groupName, direction, false);
					if (group != null)
					{
						spriteGroups.Add(group);
					}
				}
			}

			SpriteGroup.AutodetectCollectiveHorizontalOffsets(spriteGroups.ToArray());
		}

		public void SetAlignment(string groupName, Direction direction, HorizontalAlignment horizontalAlignment, VerticalAlignment verticalAlignment)
		{
			SpriteGroup group = this.GetGroup(groupName, direction, false);

			switch (horizontalAlignment)
			{
				case HorizontalAlignment.Center:
					group.HorizontallyCenter();
					break;
				case HorizontalAlignment.Left:
					group.AlignLeft();
					break;
				case HorizontalAlignment.Right:
					group.AlignRight();
					break;
				case HorizontalAlignment.Autodetect:
					group.AutodetectHorizontalOffsets();
					break;
			}

			switch (verticalAlignment)
			{
				case VerticalAlignment.Center:
					group.VerticallyCenter();
					break;
				case VerticalAlignment.Top:
					group.AlignTop();
					break;
				case VerticalAlignment.Bottom:
					group.AlignBottom();
					break;
				case VerticalAlignment.Autodetect:
					group.AutodetectVerticalOffsets();
					break;
			}
		}

		private Image ExtractSprite(int x, int y)
		{
			int left = x;
			int right = x;
			int top = y;
			int bottom = y;

			int width = this.sheetWidth;
			int height = this.sheetHeight;

			while (left >= 0 && right < width && top >= 0 && bottom < height)
			{
				// check left
				if (!this.VerticalLineIsClear(top, left, bottom))
				{
					left--;
					continue;
				}

				// check top
				if (!this.HorizontalLineIsClear(left, top, right))
				{
					top--;
					continue;
				}

				// check right
				if (!this.VerticalLineIsClear(top, right, bottom))
				{
					right++;
					continue;
				}

				// check bottom
				if (!this.HorizontalLineIsClear(left, bottom, right))
				{
					bottom++;
					continue;
				}

				left++;
				right--;
				top++;
				bottom--;

				break;
			}

			Image sprite = new Image(right - left + 1, bottom - top + 1);
			sprite.Blit(this.sheet, -left, -top);

			return sprite;
		}

		private bool HorizontalLineIsClear(int left, int y, int right)
		{
			int[] pixels = this.sheet.Pixels;

			int start = y * this.sheetWidth + left;
			int end = y * this.sheetWidth + right;
			for (int i = start; i <= end; ++i)
			{
				if (pixels[i] != 0)
				{
					return false;
				}
			}
			return true;
		}

		private bool VerticalLineIsClear(int top, int x, int bottom)
		{
			int[] pixels = this.sheet.Pixels;
			int start = top * this.sheetWidth + x;
			int end = bottom * this.sheetWidth + x;
			int width = this.sheetWidth;
			for (int i = start; i <= end; i += width)
			{
				if (pixels[i] != 0)
				{
					return false;
				}
			}
			return true;
		}

		internal Color AutodetectTransparentBackgroundColor(Image sheet)
		{
			if (sheet.Width < 10 || sheet.Height < 10)
			{
				throw new Gamelight.InvalidOperationException("The sprite sheet is too small to run the background color auto-detection algorithm");
			}

			Dictionary<int, int> totals = new Dictionary<int, int>();

			int width = sheet.Width;
			int height = sheet.Height;
			int value;
			int xMultiplier = 1;
			int yMultiplier = 1;

			foreach (int xOffset in new int[] { 0, width - 1 })
			{
				xMultiplier = xOffset == 0 ? 1 : -1;
				foreach (int yOffset in new int[] { 0, height - 1 })
				{
					yMultiplier = yOffset == 0 ? 1 : -1;
					for (int x = 0; x < 5; ++x)
					{
						for (int y = 0; y < 5; ++y)
						{
							value = sheet.GetColor(xMultiplier * (x - xOffset), yMultiplier * (y - yOffset)).IntValue;
							if (!totals.ContainsKey(value))
							{
								totals[value] = 1;
							}
							else
							{
								totals[value]++;
							}
						}
					}
				}
			}

			int maxCount = 0;
			int winner = 0;
			foreach (int colorValue in totals.Keys)
			{
				if (totals[colorValue] > maxCount)
				{
					winner = colorValue;
					maxCount = totals[colorValue];
				}
			}

			return new Color(winner);
		}

		private Image ClearOutTransparency(Image image, Color bgColor)
		{
			Image img = new Image(image);
			img.XOffset = 0;
			img.YOffset = 0;

			int bgValue = bgColor.IntValue;
			int[] pixels = img.Pixels;
			int length = pixels.Length;
			for (int i = 0; i < length; ++i)
			{
				if (pixels[i] == bgValue || ((pixels[i] >> 24) & 255) == 0)
				{
					pixels[i] = 0;
				}
			}

			return img;
		}
	}
}
